package uml.objects;

import java.awt.Color;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.HashSet;

import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import uml.GUI.DiagramPanel;
import uml.eventsystem.IObserver;
import uml.eventsystem.events.UMLEvent;
import uml.eventsystem.events.UMLEventId;
import uml.util.UMLConstants;

public class ArrowManager
		extends JPanel
		implements IObserver, MouseListener
{
	private ArrayList<AnchorPoint> m_anchorPoints;
	private DiagramPanel m_panel;
	
	/**
	 * Constructor sets the panel it is suppose to manage
	 * @param panelToManage
	 */

	public ArrowManager (DiagramPanel panelToManage)
	{
		m_panel = panelToManage;
		m_anchorPoints = new ArrayList<AnchorPoint> ();
		this.setBackground (new Color (0, 0, 0, 0));
		this.addMouseListener (this);
	}
	
	/**
	 * adds an anchorPoint and all its siblings to the DiagramPanel and itself
	 * @param anchor
	 */

	public void add (AnchorPoint anchor)
	{
		if(!isRoot(anchor))
			return;
		m_anchorPoints.add (anchor);
		addChildren (anchor);
		if(anchor.getContext() == null)
			anchor.setContext(m_panel);
	}
	
	/**
	 * Adds the children of the anchor point to this and the Diagram
	 * @param anchor
	 */

	public void addChildren (AnchorPoint anchor)
	{
		if (anchor == null)
			return;
		addChild (anchor);
		for (AnchorPoint sibling : anchor.getSiblings ())
			this.addChildren (sibling);
	}
	
	/**
	 * Adds child to the diagram panel and registers this for its events
	 * @param anchor
	 */

	public void addChild (AnchorPoint anchor)
	{
		m_panel.add (anchor.getComponent (), anchor.getLayer ());
		anchor.registerObserver (m_panel);
		anchor.addObserver (this, UMLEventId.REPAINT);
		anchor.notifyObservers (UMLEventId.ANCHOR);
	}
	
	/**
	 * paints all the arrows in the diagram
	 */

	public void paint (Graphics g)
	{
		super.paint (g);
		this.setPreferredSize (m_panel.getSize ());
		if (g instanceof Graphics2D)
		{
			Graphics2D g2D = (Graphics2D) g;
			for (AnchorPoint anchor : m_anchorPoints)
			{
				g2D.setStroke (anchor.getStroke ());
				//handle an arrowEnd at root anchorPoint
				g.setColor (UMLConstants.ARROW_COLOR);
				paintArrow (anchor, g2D);
				if (anchor.getArrowEnd () != null)
					rootArrowEnd (anchor, g);
			}
		}
	}
	
	/**
	 * Calls remove(AnchorPoint anchor)
	 * @param p
	 */

	public void delete (AnchorPoint p)
	{
		if(p != null)
			remove(p);
	}
	
	/**
	 * removes an anchor point
	 * @param anchor
	 */
	
	public void remove(AnchorPoint anchor)
	{
		if(isRoot(anchor))
		{
			for(AnchorPoint sibling : anchor.getSiblings())
			{
				if(sibling.getSiblings().isEmpty())
					remove(sibling);
				else
				{
					m_anchorPoints.add(sibling);
					anchor.removeSibling(sibling);
				}
			}
			m_anchorPoints.remove(anchor);
		}
		else
			if(anchor.getSiblings().isEmpty() && isRoot(anchor.getAnchorParent()) && anchor.getAnchorParent().getSiblings().size() == 1)
			{
				AnchorPoint parent = anchor.getAnchorParent();
				parent.removeSibling(anchor);
				remove(parent);
			}
			else
			{
				for(AnchorPoint sibling : anchor.getSiblings())
				{
					anchor.removeSibling(sibling);
					anchor.getAnchorParent().addSibling(sibling);
				}
				anchor.getAnchorParent().removeSibling(anchor);
			}
		m_panel.remove(anchor.getComponent());
	}
	
	/**
	 * returns true if anchor does not have a parent
	 * @param anchor
	 * @return
	 */
	
	public boolean isRoot(AnchorPoint anchor)
	{
		if(anchor == null)
		{
			System.out.println("anchor is null");
			return false;
		}
		return anchor.getAnchorParent() == null;
	}
	
	/**
	 * paints the ArrowEnd for a root AnchorPoint
	 * @param anchor
	 * @param g
	 */
	
	private void rootArrowEnd (AnchorPoint anchor, Graphics g)
	{
		Point begin;
		Point end;
		Container context;
		Point p1;
		Point p2;

		if (g instanceof Graphics2D)//otherwise we cant draw it
		{
			Graphics2D g2D = (Graphics2D) g;

			if (anchor.getSiblings ().size () > 0)
			{
				begin = SwingUtilities.convertPoint (anchor.getContext (), anchor.getPoint (), this);
				end = SwingUtilities.convertPoint (anchor.getSiblings ().get (0).getContext (), anchor.getSiblings ()
						.get (0).getPoint (), this);//first sibling's point
				anchor.getArrowEnd ().paint (g2D, end.x, end.y, begin.x, begin.y);
			}
			//draw the arrow end aligned to the first sibling

		}
	}
	
	/**
	 * Paints an arrow defined by a root anchorpoint and the anchor points connected to it
	 * @param anchor
	 * @param g
	 */

	private void paintArrow (AnchorPoint anchor, Graphics g)
	{
		Point begin = new Point ();
		Point end = new Point ();
		if (anchor == null)
		{
			return;
		}
		else
		{
			ArrayList<AnchorPoint> siblings = anchor.getSiblings ();
			for (AnchorPoint sibling : siblings)
			{
				//convert the anchorPoint coordinates to this components coordinates
				begin = SwingUtilities.convertPoint (anchor.getContext (), anchor.getPoint (), this);
				end = SwingUtilities.convertPoint (sibling.getContext (), sibling.getPoint (), this);
				g.drawLine (begin.x, begin.y, end.x, end.y);
				paintArrow (sibling, g);
				ArrowEnd anchorEnd  = siblings.get (siblings.size ()-1).getArrowEnd ();
				if (anchorEnd != null)
				{
					anchorEnd.paint ((Graphics2D)g, begin.x, begin.y, end.x, end.y);
				}
			}
		}
	}
	

	@Override
	public void handleNotification (UMLEvent event)
	{
		if (event.getEventId () == UMLEventId.REPAINT)
			this.repaint ();
	}

	@Override
	public void mouseClicked (MouseEvent event)
	{
		m_panel.mouseClicked(event);
	}

	@Override
	public void mouseEntered (MouseEvent event)
	{

	}

	@Override
	public void mouseExited (MouseEvent event)
	{

	}

	@Override
	public void mousePressed (MouseEvent event)
	{
		if (event.isAltDown ())
		{
			Point clickLocation = event.getPoint ();
			AnchorPointPair lineToSplit = findLineNearPoint (clickLocation, 10);
			if(lineToSplit != null)
				splitLine (lineToSplit, clickLocation);
		}
		else
			m_panel.mousePressed(event);
	}

	private void splitLine (AnchorPointPair lineToSplit, Point location)
	{
		AnchorPoint newPoint = new AnchorPoint (location, this.getParent (), lineToSplit.point1.getStroke (), null);
		newPoint.addSibling (lineToSplit.point2);
		lineToSplit.point1.addSibling (newPoint);
		lineToSplit.point1.removeSibling (lineToSplit.point2);
		addChild (newPoint);
		m_panel.validate ();
		UMLEvent event = new UMLEvent (UMLEventId.SELECT, newPoint);
		m_panel.handleNotification (event);
		this.repaint ();
	}

	private AnchorPointPair findLineNearPoint (Point location, float distance)
	{
		for (AnchorPoint point : m_anchorPoints)
		{
			AnchorPointPair anchor = findIfPointNearLine (point, location, distance);
			if (anchor != null)
				return anchor;
		}
		return null;
	}

	private AnchorPointPair findIfPointNearLine (AnchorPoint anchor, Point location, float distance)
	{
		if (anchor == null)
			return null;
		for (AnchorPoint sibling : anchor.getSiblings ())
		{
			Point p1 = SwingUtilities.convertPoint (anchor.getContext (), anchor.getPoint (), this);
			Point p2 = SwingUtilities.convertPoint (sibling.getContext (), sibling.getPoint (), this);
			int distanceBetweenLine = (int) Line2D.ptLineDist (p1.x, p1.y, p2.x, p2.y, location.x, location.y);
			if (distanceBetweenLine <= distance)
				return new AnchorPointPair (anchor, sibling);
			AnchorPointPair temp = findIfPointNearLine (sibling, location, distance);
			if (temp != null)
				return temp;
		}
		return null;
	}

	@Override
	public void mouseReleased (MouseEvent event)
	{

	}

	private class AnchorPointPair
	{
		public AnchorPoint point1;
		public AnchorPoint point2;

		public AnchorPointPair (AnchorPoint p1, AnchorPoint p2)
		{
			point1 = p1;
			point2 = p2;
		}
	}
}
